/*
 * @(#)ImageView.java	1.58 05/11/30
 *
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package org.agnitas.cms.utils.preview;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import java.awt.*;
import java.awt.image.*;
import java.net.*;
import java.util.*;

/**
 * View of an Image, intended to support the HTML &lt;IMG&gt; tag.
 * Supports scaling via the HEIGHT and WIDTH attributes of the tag.
 * If the image is unable to be loaded any text specified via the
 * <code>ALT</code> attribute will be rendered.
 * <p/>
 * While this class has been part of swing for a while now, it is public
 * as of 1.4.
 *
 * @author Scott Violet
 * @version 1.58 11/30/05
 * @see javax.swing.text.IconView
 * @since 1.4
 */
public class CmsImageView extends View {
	/**
	 * If true, when some of the bits are available a repaint is done.
	 * <p/>
	 * This is set to false as swing does not offer a repaint that takes a
	 * delay. If this were true, a bunch of immediate repaints would get
	 * generated that end up significantly delaying the loading of the image
	 * (or anything else going on for that matter).
	 */
	private static boolean sIsInc = false;
	/**
	 * Repaint delay when some of the bits are available.
	 */
	private static int sIncRate = 100;
	/**
	 * Property name for pending image icon
	 */
	private static final String PENDING_IMAGE = "html.pendingImage";
	/**
	 * Property name for missing image icon
	 */
	private static final String MISSING_IMAGE = "html.missingImage";

	/**
	 * Document property for image cache.
	 */
	private static final String IMAGE_CACHE_PROPERTY = "imageCache";

	// Height/width to use before we know the real size, these should at least
	// the size of <code>sMissingImageIcon</code> and
	// <code>sPendingImageIcon</code>
	private static final int DEFAULT_WIDTH = 38;
	private static final int DEFAULT_HEIGHT = 38;

	/**
	 * Default border to use if one is not specified.
	 */
	private static final int DEFAULT_BORDER = 2;

	// Bitmask values
	private static final int LOADING_FLAG = 1;
	private static final int LINK_FLAG = 2;
	private static final int WIDTH_FLAG = 4;
	private static final int HEIGHT_FLAG = 8;
	private static final int RELOAD_FLAG = 16;
	private static final int RELOAD_IMAGE_FLAG = 32;
	private static final int SYNC_LOAD_FLAG = 64;

	private AttributeSet attr;
	private Image image;
	private int width;
	private int height;
	/**
	 * Bitmask containing some of the above bitmask values. Because the
	 * image loading notification can happen on another thread access to
	 * this is synchronized (at least for modifying it).
	 */
	private int state;
	private Container container;
	private Rectangle fBounds;
	private Color borderColor;
	// Size of the border, the insets contains this valid. For example, if
	// the HSPACE attribute was 4 and BORDER 2, leftInset would be 6.
	private short borderSize;
	// Insets, obtained from the painter.
	private short leftInset;
	private short rightInset;
	private short topInset;
	private short bottomInset;
	/**
	 * We don't directly implement ImageObserver, instead we use an instance
	 * that calls back to us.
	 */
	private ImageObserver imageObserver;
	/**
	 * Used for alt text. Will be non-null if the image couldn't be found,
	 * and there is valid alt text.
	 */
	private View altView;
	/**
	 * Alignment along the vertical (Y) axis.
	 */
	private float vAlign;


	/**
	 * Creates a new view that represents an IMG element.
	 *
	 * @param elem the element to create a view for
	 */
	public CmsImageView(Element elem) {
		super(elem);
		fBounds = new Rectangle();
		imageObserver = new ImageHandler();
		state = RELOAD_FLAG | RELOAD_IMAGE_FLAG;
	}

	/**
	 * Returns the text to display if the image can't be loaded. This is
	 * obtained from the Elements attribute set with the attribute name
	 * <code>HTML.Attribute.ALT</code>.
	 */
	public String getAltText() {
		return (String) getElement().getAttributes().getAttribute
				(HTML.Attribute.ALT);
	}

	/**
	 * Return a URL for the image source,
	 * or null if it could not be determined.
	 */
	public URL getImageURL() {
		String src = (String) getElement().getAttributes().
				getAttribute(HTML.Attribute.SRC);
		if(src == null) {
			return null;
		}

		URL reference = ((HTMLDocument) getDocument()).getBase();
		try {
			URL u = new URL(reference, src);
			return u;
		} catch(MalformedURLException e) {
			return null;
		}
	}

	/**
	 * Returns the icon to use if the image couldn't be found.
	 */
	public Icon getNoImageIcon() {
		return (Icon) UIManager.getLookAndFeelDefaults().get(MISSING_IMAGE);
	}

	/**
	 * Returns the icon to use while in the process of loading the image.
	 */
	public Icon getLoadingImageIcon() {
		return (Icon) UIManager.getLookAndFeelDefaults().get(PENDING_IMAGE);
	}

	/**
	 * Returns the image to render.
	 */
	public Image getImage() {
		sync();
		return image;
	}

	/**
	 * Sets how the image is loaded. If <code>newValue</code> is true,
	 * the image we be loaded when first asked for, otherwise it will
	 * be loaded asynchronously. The default is to not load synchronously,
	 * that is to load the image asynchronously.
	 */
	public void setLoadsSynchronously(boolean newValue) {
		synchronized(this) {
			if(newValue) {
				state |= SYNC_LOAD_FLAG;
			} else {
				state = (state | SYNC_LOAD_FLAG) ^ SYNC_LOAD_FLAG;
			}
		}
	}

	/**
	 * Returns true if the image should be loaded when first asked for.
	 */
	public boolean getLoadsSynchronously() {
		return ((state & SYNC_LOAD_FLAG) != 0);
	}

	/**
	 * Convenience method to get the StyleSheet.
	 */
	protected StyleSheet getStyleSheet() {
		HTMLDocument doc = (HTMLDocument) getDocument();
		return doc.getStyleSheet();
	}

	/**
	 * Fetches the attributes to use when rendering.  This is
	 * implemented to multiplex the attributes specified in the
	 * model with a StyleSheet.
	 */
	public AttributeSet getAttributes() {
		sync();
		return attr;
	}

	/**
	 * For images the tooltip text comes from text specified with the
	 * <code>ALT</code> attribute. This is overriden to return
	 * <code>getAltText</code>.
	 *
	 * @see javax.swing.text.JTextComponent#getToolTipText
	 */
	public String getToolTipText(float x, float y, Shape allocation) {
		return getAltText();
	}

	/**
	 * Update any cached values that come from attributes.
	 */
	protected void setPropertiesFromAttributes() {
		StyleSheet sheet = getStyleSheet();
		this.attr = sheet.getViewAttributes(this);

		// Gutters
		borderSize = (short) getIntAttr(HTML.Attribute.BORDER, isLink() ?
				DEFAULT_BORDER : 0);

		leftInset = rightInset = (short) (getIntAttr(HTML.Attribute.HSPACE,
				0) + borderSize);
		topInset = bottomInset = (short) (getIntAttr(HTML.Attribute.VSPACE,
				0) + borderSize);

		borderColor = ((StyledDocument) getDocument()).getForeground
				(getAttributes());

		AttributeSet attr = getElement().getAttributes();

		// Alignment.
		// PENDING: This needs to be changed to support the CSS versions
		// when conversion from ALIGN to VERTICAL_ALIGN is complete.
		Object alignment = attr.getAttribute(HTML.Attribute.ALIGN);

		vAlign = 1.0f;
		if(alignment != null) {
			alignment = alignment.toString();
			if("top".equals(alignment)) {
				vAlign = 0f;
			} else if("middle".equals(alignment)) {
				vAlign = .5f;
			}
		}

		AttributeSet anchorAttr = (AttributeSet) attr.getAttribute(HTML.Tag.A);
		if(anchorAttr != null && anchorAttr.isDefined
				(HTML.Attribute.HREF)) {
			synchronized(this) {
				state |= LINK_FLAG;
			}
		} else {
			synchronized(this) {
				state = (state | LINK_FLAG) ^ LINK_FLAG;
			}
		}
	}

	/**
	 * Establishes the parent view for this view.
	 * Seize this moment to cache the AWT Container I'm in.
	 */
	public void setParent(View parent) {
		View oldParent = getParent();
		super.setParent(parent);
		container = (parent != null) ? getContainer() : null;
		if(oldParent != parent) {
			synchronized(this) {
				state |= RELOAD_FLAG;
			}
		}
	}

	/**
	 * Invoked when the Elements attributes have changed. Recreates the image.
	 */
	public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) {
		super.changedUpdate(e, a, f);

		synchronized(this) {
			state |= RELOAD_FLAG | RELOAD_IMAGE_FLAG;
		}

		// Assume the worst.
		preferenceChanged(null, true, true);
	}

	/**
	 * Paints the View.
	 *
	 * @param g the rendering surface to use
	 * @param a the allocated region to render into
	 * @see javax.swing.text.View#paint
	 */
	public void paint(Graphics g, Shape a) {
		sync();

		Rectangle rect = (a instanceof Rectangle) ? (Rectangle) a :
				a.getBounds();

		Image image = getImage();
		Rectangle clip = g.getClipBounds();

		fBounds.setBounds(rect);
		paintHighlights(g, a);
		paintBorder(g, rect);
		if(clip != null) {
			g.clipRect(rect.x + leftInset, rect.y + topInset,
					rect.width - leftInset - rightInset,
					rect.height - topInset - bottomInset);
		}
		if(image != null) {
			if(!hasPixels(image)) {
				// No pixels yet, use the default
				Icon icon = (image == null) ? getNoImageIcon() :
						getLoadingImageIcon();

				if(icon != null) {
					icon.paintIcon(getContainer(), g, rect.x + leftInset,
							rect.y + topInset);
				}
			} else {
				// Draw the image
				g.drawImage(image, rect.x + leftInset, rect.y + topInset,
						width, height, imageObserver);
			}
		} else {
			Icon icon = getNoImageIcon();

			if(icon != null) {
				icon.paintIcon(getContainer(), g, rect.x + leftInset,
						rect.y + topInset);
			}
			View view = getAltView();
			// Paint the view representing the alt text, if its non-null
			if(view != null && ((state & WIDTH_FLAG) == 0 ||
					width > DEFAULT_WIDTH)) {
				// Assume layout along the y direction
				Rectangle altRect = new Rectangle
						(rect.x + leftInset + DEFAULT_WIDTH, rect.y + topInset,
								rect.width - leftInset - rightInset - DEFAULT_WIDTH,
								rect.height - topInset - bottomInset);

				view.paint(g, altRect);
			}
		}
		if(clip != null) {
			// Reset clip.
			g.setClip(clip.x, clip.y, clip.width, clip.height);
		}
	}

	private void paintHighlights(Graphics g, Shape shape) {
		if(container instanceof JTextComponent) {
			JTextComponent tc = (JTextComponent) container;
			Highlighter h = tc.getHighlighter();
			if(h instanceof LayeredHighlighter) {
				((LayeredHighlighter) h).paintLayeredHighlights
						(g, getStartOffset(), getEndOffset(), shape, tc, this);
			}
		}
	}

	private void paintBorder(Graphics g, Rectangle rect) {
		Color color = borderColor;

		if((borderSize > 0 || image == null) && color != null) {
			int xOffset = leftInset - borderSize;
			int yOffset = topInset - borderSize;
			g.setColor(color);
			int n = (image == null) ? 1 : borderSize;
			for(int counter = 0; counter < n; counter++) {
				g.drawRect(rect.x + xOffset + counter,
						rect.y + yOffset + counter,
						rect.width - counter - counter - xOffset - xOffset - 1,
						rect.height - counter - counter - yOffset - yOffset - 1);
			}
		}
	}

	/**
	 * Determines the preferred span for this view along an
	 * axis.
	 *
	 * @param axis may be either X_AXIS or Y_AXIS
	 * @return the span the view would like to be rendered into;
	 *         typically the view is told to render into the span
	 *         that is returned, although there is no guarantee;
	 *         the parent may choose to resize or break the view
	 */
	public float getPreferredSpan(int axis) {
		sync();

		// If the attributes specified a width/height, always use it!
		if(axis == View.X_AXIS && (state & WIDTH_FLAG) == WIDTH_FLAG) {
			getPreferredSpanFromAltView(axis);
			return width + leftInset + rightInset;
		}
		if(axis == View.Y_AXIS && (state & HEIGHT_FLAG) == HEIGHT_FLAG) {
			getPreferredSpanFromAltView(axis);
			return height + topInset + bottomInset;
		}

		Image image = getImage();

		if(image != null) {
			switch(axis) {
				case View.X_AXIS:
					return width + leftInset + rightInset;
				case View.Y_AXIS:
					return height + topInset + bottomInset;
				default:
					throw new IllegalArgumentException("Invalid axis: " + axis);
			}
		} else {
			View view = getAltView();
			float retValue = 0f;

			if(view != null) {
				retValue = view.getPreferredSpan(axis);
			}
			switch(axis) {
				case View.X_AXIS:
					return retValue + (float) (width + leftInset + rightInset);
				case View.Y_AXIS:
					return retValue + (float) (height + topInset + bottomInset);
				default:
					throw new IllegalArgumentException("Invalid axis: " + axis);
			}
		}
	}

	/**
	 * Determines the desired alignment for this view along an
	 * axis.  This is implemented to give the alignment to the
	 * bottom of the icon along the y axis, and the default
	 * along the x axis.
	 *
	 * @param axis may be either X_AXIS or Y_AXIS
	 * @return the desired alignment; this should be a value
	 *         between 0.0 and 1.0 where 0 indicates alignment at the
	 *         origin and 1.0 indicates alignment to the full span
	 *         away from the origin; an alignment of 0.5 would be the
	 *         center of the view
	 */
	public float getAlignment(int axis) {
		switch(axis) {
			case View.Y_AXIS:
				return vAlign;
			default:
				return super.getAlignment(axis);
		}
	}

	/**
	 * Provides a mapping from the document model coordinate space
	 * to the coordinate space of the view mapped to it.
	 *
	 * @param pos the position to convert
	 * @param a   the allocated region to render into
	 * @return the bounding box of the given position
	 * @throws javax.swing.text.BadLocationException
	 *          if the given position does not represent a
	 *          valid location in the associated document
	 * @see javax.swing.text.View#modelToView
	 */
	public Shape modelToView(int pos, Shape a, Position.Bias b) throws
			BadLocationException {
		int p0 = getStartOffset();
		int p1 = getEndOffset();
		if((pos >= p0) && (pos <= p1)) {
			Rectangle r = a.getBounds();
			if(pos == p1) {
				r.x += r.width;
			}
			r.width = 0;
			return r;
		}
		return null;
	}

	/**
	 * Provides a mapping from the view coordinate space to the logical
	 * coordinate space of the model.
	 *
	 * @param x the X coordinate
	 * @param y the Y coordinate
	 * @param a the allocated region to render into
	 * @return the location within the model that best represents the
	 *         given point of view
	 * @see javax.swing.text.View#viewToModel
	 */
	public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
		Rectangle alloc = (Rectangle) a;
		if(x < alloc.x + alloc.width) {
			bias[0] = Position.Bias.Forward;
			return getStartOffset();
		}
		bias[0] = Position.Bias.Backward;
		return getEndOffset();
	}

	/**
	 * Sets the size of the view.  This should cause
	 * layout of the view if it has any layout duties.
	 *
	 * @param width  the width >= 0
	 * @param height the height >= 0
	 */
	public void setSize(float width, float height) {
		sync();

		if(getImage() == null) {
			View view = getAltView();

			if(view != null) {
				view.setSize(Math.max(0f,
						width - (float) (DEFAULT_WIDTH + leftInset + rightInset)),
						Math.max(0f, height - (float) (topInset + bottomInset)));
			}
		}
	}

	/**
	 * Returns true if this image within a link?
	 */
	private boolean isLink() {
		return ((state & LINK_FLAG) == LINK_FLAG);
	}

	/**
	 * Returns true if the passed in image has a non-zero width and height.
	 */
	private boolean hasPixels(Image image) {
		return image != null &&
				(image.getHeight(imageObserver) > 0) &&
				(image.getWidth(imageObserver) > 0);
	}

	/**
	 * Returns the preferred span of the View used to display the alt text,
	 * or 0 if the view does not exist.
	 */
	private float getPreferredSpanFromAltView(int axis) {
		if(getImage() == null) {
			View view = getAltView();

			if(view != null) {
				return view.getPreferredSpan(axis);
			}
		}
		return 0f;
	}

	/**
	 * Request that this view be repainted.
	 * Assumes the view is still at its last-drawn location.
	 */
	private void repaint(long delay) {
		if(container != null && fBounds != null) {
			container.repaint(delay, fBounds.x, fBounds.y, fBounds.width,
					fBounds.height);
		}
	}

	public void onImageLoaded() {

	}

	/**
	 * Convenience method for getting an integer attribute from the elements
	 * AttributeSet.
	 */
	private int getIntAttr(HTML.Attribute name, int deflt) {
		AttributeSet attr = getElement().getAttributes();
		if(attr.isDefined(name)) {		// does not check parents!
			int i;
			String val = (String) attr.getAttribute(name);
			if(val == null) {
				i = deflt;
			} else {
				try {
					i = Math.max(0, Integer.parseInt(val));
				} catch(NumberFormatException x) {
					i = deflt;
				}
			}
			return i;
		} else
			return deflt;
	}

	/**
	 * Makes sure the necessary properties and image is loaded.
	 */
	private void sync() {
		int s = state;
		if((s & RELOAD_IMAGE_FLAG) != 0) {
			refreshImage();
		}
		s = state;
		if((s & RELOAD_FLAG) != 0) {
			synchronized(this) {
				state = (state | RELOAD_FLAG) ^ RELOAD_FLAG;
			}
			setPropertiesFromAttributes();
		}
	}

	/**
	 * Loads the image and updates the size accordingly. This should be
	 * invoked instead of invoking <code>loadImage</code> or
	 * <code>updateImageSize</code> directly.
	 */
	private void refreshImage() {
		synchronized(this) {
			// clear out width/height/realoadimage flag and set loading flag
			state = (state | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG |
					HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG |
					RELOAD_IMAGE_FLAG);
			image = null;
			width = height = 0;
		}

		try {
			// Load the image
			loadImage();

			// And update the size params
			updateImageSize();
		}
		finally {
			synchronized(this) {
				// Clear out state in case someone threw an exception.
				state = (state | LOADING_FLAG) ^ LOADING_FLAG;
			}
		}
	}

	/**
	 * Loads the image from the URL <code>getImageURL</code>. This should
	 * only be invoked from <code>refreshImage</code>.
	 */
	private void loadImage() {
		URL src = getImageURL();
		Image newImage = null;
		if(src != null) {
			Dictionary cache = (Dictionary) getDocument().
					getProperty(IMAGE_CACHE_PROPERTY);
			if(cache != null) {
				newImage = (Image) cache.get(src);
			} else {
				newImage = Toolkit.getDefaultToolkit().createImage(src);
				if(newImage != null && getLoadsSynchronously()) {
					// Force the image to be loaded by using an ImageIcon.
					ImageIcon ii = new ImageIcon();
					ii.setImage(newImage);
				}
			}
		}
		image = newImage;
	}

	/**
	 * Recreates and reloads the image.  This should
	 * only be invoked from <code>refreshImage</code>.
	 */
	private void updateImageSize() {
		int newWidth = 0;
		int newHeight = 0;
		int newState = 0;
		Image newImage = getImage();

		if(newImage != null) {
			Element elem = getElement();
			AttributeSet attr = elem.getAttributes();

			// Get the width/height and set the state ivar before calling
			// anything that might cause the image to be loaded, and thus the
			// ImageHandler to be called.
			newWidth = getIntAttr(HTML.Attribute.WIDTH, -1);
			if(newWidth > 0) {
				newState |= WIDTH_FLAG;
			}
			newHeight = getIntAttr(HTML.Attribute.HEIGHT, -1);
			if(newHeight > 0) {
				newState |= HEIGHT_FLAG;
			}

			if(newWidth <= 0) {
				newWidth = newImage.getWidth(imageObserver);
				if(newWidth <= 0) {
					newWidth = DEFAULT_WIDTH;
				}
			}

			if(newHeight <= 0) {
				newHeight = newImage.getHeight(imageObserver);
				if(newHeight <= 0) {
					newHeight = DEFAULT_HEIGHT;
				}
			}

			// Make sure the image starts loading:
			if((newState & (WIDTH_FLAG | HEIGHT_FLAG)) != 0) {
				Toolkit.getDefaultToolkit().prepareImage(newImage, newWidth,
						newHeight,
						imageObserver);
			} else {
				Toolkit.getDefaultToolkit().prepareImage(newImage, -1, -1,
						imageObserver);
			}

			boolean createText = false;
			synchronized(this) {
				// If imageloading failed, other thread may have called
				// ImageLoader which will null out image, hence we check
				// for it.
				if(image != null) {
					if((newState & WIDTH_FLAG) == WIDTH_FLAG || width == 0) {
						width = newWidth;
					}
					if((newState & HEIGHT_FLAG) == HEIGHT_FLAG ||
							height == 0) {
						height = newHeight;
					}
				} else {
					createText = true;
					if((newState & WIDTH_FLAG) == WIDTH_FLAG) {
						width = newWidth;
					}
					if((newState & HEIGHT_FLAG) == HEIGHT_FLAG) {
						height = newHeight;
					}
				}
				state = state | newState;
				state = (state | LOADING_FLAG) ^ LOADING_FLAG;
			}
			if(createText) {
				// Only reset if this thread determined image is null
				updateAltTextView();
			}
		} else {
			width = height = DEFAULT_HEIGHT;
			updateAltTextView();
		}
	}

	/**
	 * Updates the view representing the alt text.
	 */
	private void updateAltTextView() {
		String text = getAltText();

		if(text != null) {
			ImageLabelView newView;

			newView = new ImageLabelView(getElement(), text);
			synchronized(this) {
				altView = newView;
			}
		}
	}

	/**
	 * Returns the view to use for alternate text. This may be null.
	 */
	private View getAltView() {
		View view;

		synchronized(this) {
			view = altView;
		}
		if(view != null && view.getParent() == null) {
			view.setParent(getParent());
		}
		return view;
	}

	/**
	 * Invokes <code>preferenceChanged</code> on the event displatching
	 * thread.
	 */
	private void safePreferenceChanged() {
		if(SwingUtilities.isEventDispatchThread()) {
			Document doc = getDocument();
			if(doc instanceof AbstractDocument) {
				((AbstractDocument) doc).readLock();
			}
			preferenceChanged(null, true, true);
			if(doc instanceof AbstractDocument) {
				((AbstractDocument) doc).readUnlock();
			}
		} else {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					safePreferenceChanged();
				}
			});
		}
	}

	/**
	 * ImageHandler implements the ImageObserver to correctly update the
	 * display as new parts of the image become available.
	 */
	private class ImageHandler implements ImageObserver {
		// This can come on any thread. If we are in the process of reloading
		// the image and determining our state (loading == true) we don't fire
		// preference changed, or repaint, we just reset the fWidth/fHeight as
		// necessary and return. This is ok as we know when loading finishes
		// it will pick up the new height/width, if necessary.
		public boolean imageUpdate(Image img, int flags, int x, int y,
								   int newWidth, int newHeight) {
			if(image == null || image != img || getParent() == null) {
				return false;
			}

			// Bail out if there was an error:
			if((flags & (ABORT | ERROR)) != 0) {
				repaint(0);
				synchronized(CmsImageView.this) {
					if(image == img) {
						// Be sure image hasn't changed since we don't
						// initialy synchronize
						image = null;
						if((state & WIDTH_FLAG) != WIDTH_FLAG) {
							width = DEFAULT_WIDTH;
						}
						if((state & HEIGHT_FLAG) != HEIGHT_FLAG) {
							height = DEFAULT_HEIGHT;
						}
					}
					if((state & LOADING_FLAG) == LOADING_FLAG) {
						// No need to resize or repaint, still in the process
						// of loading.
						return false;
					}
				}
				updateAltTextView();
				safePreferenceChanged();
				onImageLoaded();
				return false;
			}

			// Resize image if necessary:
			short changed = 0;
			if((flags & ImageObserver.HEIGHT) != 0 && !getElement().
					getAttributes().isDefined(HTML.Attribute.HEIGHT)) {
				changed |= 1;
			}
			if((flags & ImageObserver.WIDTH) != 0 && !getElement().
					getAttributes().isDefined(HTML.Attribute.WIDTH)) {
				changed |= 2;
			}

			synchronized(CmsImageView.this) {
				if(image != img) {
					return false;
				}
				if((changed & 1) == 1 && (state & WIDTH_FLAG) == 0) {
					width = newWidth;
				}
				if((changed & 2) == 2 && (state & HEIGHT_FLAG) == 0) {
					height = newHeight;
				}
				if((state & LOADING_FLAG) == LOADING_FLAG) {
					// No need to resize or repaint, still in the process of
					// loading.
					return true;
				}
			}
			if(changed != 0) {
				// May need to resize myself, asynchronously:
				safePreferenceChanged();
				return true;
			}

			// Repaint when done or when new pixels arrive:
			if((flags & (FRAMEBITS | ALLBITS)) != 0) {
				repaint(0);
				onImageLoaded();
			} else if((flags & SOMEBITS) != 0 && sIsInc) {
				repaint(sIncRate);
			}
			return ((flags & ALLBITS) == 0);
		}
	}


	/**
	 * ImageLabelView is used if the image can't be loaded, and
	 * the attribute specified an alt attribute. It overriden a handle of
	 * methods as the text is hardcoded and does not come from the document.
	 */
	private class ImageLabelView extends InlineView {
		private Segment segment;
		private Color fg;

		ImageLabelView(Element e, String text) {
			super(e);
			reset(text);
		}

		public void reset(String text) {
			segment = new Segment(text.toCharArray(), 0, text.length());
		}

		public void paint(Graphics g, Shape a) {
			// Don't use supers paint, otherwise selection will be wrong
			// as our start/end offsets are fake.
			GlyphPainter painter = getGlyphPainter();

			if(painter != null) {
				g.setColor(getForeground());
				painter.paint(this, g, a, getStartOffset(), getEndOffset());
			}
		}

		public Segment getText(int p0, int p1) {
			if(p0 < 0 || p1 > segment.array.length) {
				throw new RuntimeException("ImageLabelView: Stale view");
			}
			segment.offset = p0;
			segment.count = p1 - p0;
			return segment;
		}

		public int getStartOffset() {
			return 0;
		}

		public int getEndOffset() {
			return segment.array.length;
		}

		public View breakView(int axis, int p0, float pos, float len) {
			// Don't allow a break
			return this;
		}

		public Color getForeground() {
			View parent;
			if(fg == null && (parent = getParent()) != null) {
				Document doc = getDocument();
				AttributeSet attr = parent.getAttributes();

				if(attr != null && (doc instanceof StyledDocument)) {
					fg = ((StyledDocument) doc).getForeground(attr);
				}
			}
			return fg;
		}
	}
}